﻿using System;
using System.Text;
using System.Data;
using System.Net;
using System.Text.RegularExpressions;
using System.IO;
using System.Net.Mail;

namespace WebSiteUpdateChecker
{
    class WebPage
    {
        #region フィールド、プロパティ

        /// <summary>siteDataTableのレコード。</summary>
        public DataRow DataRow { get; private set; }

        /// <summary>
        /// URLを取得、設定する。
        /// 設定したURLが前回と異なる場合は、オブジェクトが更新される。
        /// </summary>
        public string Url
        {
            get { return this.DataRow["Url"] != DBNull.Value ? (string)this.DataRow["Url"] : ""; }
            set
            {
                this.DataRow["Url"] = value;
                if (DataRow["Url"].ToString() != value)
                {
                    this.CheckedDate = DateTime.MinValue;
                    this.LatestDate = DateTime.MinValue;
                    this.LatestHtml = "";
                    this.LatestDiff = "";

                }
            }
        }

        /// <summary>
        /// タイトルを取得、設定する。
        /// </summary>
        public string Title
        {
            get { return (string)this.DataRow["Title"]; }
            set { this.DataRow["Title"] = value; }
        }

        /// <summary>
        /// 対象範囲開始文字列フィルタを取得、設定する。
        /// </summary>
        public string FilterStart
        {
            get { return (string)this.DataRow["FilterStart"]; }
            set { this.DataRow["FilterStart"] = value; }
        }

        /// <summary>
        /// 対象範囲終了文字列フィルタを取得、設定する。
        /// </summary>
        public string FilterEnd
        {
            get { return (string)this.DataRow["FilterEnd"]; }
            set { this.DataRow["FilterEnd"] = value; }
        }

        /// <summary>
        /// 対象文字列フィルタを取得、設定する。
        /// </summary>
        public string FilterInclude
        {
            get { return (string)this.DataRow["FilterInclude"]; }
            set { this.DataRow["FilterInclude"] = value; }
        }

        /// <summary>
        /// 無視文字列フィルタを取得、設定する。
        /// </summary>
        public string FilterIgnore
        {
            get { return (string)this.DataRow["FilterIgnore"]; }
            set { this.DataRow["FilterIgnore"] = value; }
        }

        /// <summary>
        /// コメントを取得、設定する。
        /// </summary>
        public string Comment
        {
            get { return (string)this.DataRow["Comment"]; }
            set { this.DataRow["Comment"] = value; }
        }

        /// <summary>
        /// 動作ステータスを取得、設定する。
        /// </summary>
        public bool Status
        {
            get { return (bool)this.DataRow["Status"]; }
            set { this.DataRow["Status"] = value; }
        }

        /// <summary>
        /// 確認日時を取得、設定する。
        /// 設定されていない場合は<see cref="DateTime.MinValue"/>を返す。
        /// <see cref="DateTime.MinValue"/>を設定した場合は消去する。
        /// </summary>
        public DateTime CheckedDate
        {
            get
            {
                if (this.DataRow["CheckedDate"] != DBNull.Value) return (DateTime)this.DataRow["CheckedDate"];
                else return DateTime.MinValue;
            }
            set
            {
                if (value != DateTime.MinValue) this.DataRow["CheckedDate"] = value;
                else this.DataRow["CheckedDate"] = DBNull.Value;
            }

        }

        /// <summary>
        /// 更新日時を取得、設定する。
        /// 設定されていない場合は<see cref="DateTime.MinValue"/>を返す。
        /// <see cref="DateTime.MinValue"/>を設定した場合は消去する。
        /// </summary>
        public DateTime LatestDate
        {
            get
            {
                if (this.DataRow["LatestDate"] != DBNull.Value) return (DateTime)this.DataRow["LatestDate"];
                else return DateTime.MinValue;
            }
            set
            {
                if (value != DateTime.MinValue) this.DataRow["LatestDate"] = value;
                else this.DataRow["LatestDate"] = DBNull.Value;
            }
        }

        /// <summary>
        /// 更新ページのHTMLを取得、設定する。
        /// </summary>
        public string LatestHtml
        {
            get { return (string)this.DataRow["LatestPage"]; }
            set { this.DataRow["LatestPage"] = value; }
        }

        /// <summary>
        /// 更新差分を取得、設定する。
        /// </summary>
        public string LatestDiff
        {
            get { return (string)this.DataRow["LatestDiff"]; }
            set { this.DataRow["LatestDiff"] = value; }
        }

        #endregion



        /// <summary>
        /// ウェブページを表すクラス。
        /// </summary>
        /// <param name="row">siteDataTableのレコード。</param>
        public WebPage(DataRow row)
        {
            this.DataRow = row;
        }

        /// <summary>
        /// ページの更新を確認する。
        /// 取得したページおよび差分内容はオブジェクトに保存される。
        /// </summary>
        /// <returns>変更があった場合はtrue。</returns>
        public bool CheckLatestPage()
        {
            // HTMLを取得
            string html = GetPageHtml(this.Url);
            // BODY要素を取得
            Regex regexp = new Regex("<body[^>]*>(.*?)</body>",
                RegexOptions.IgnoreCase | RegexOptions.Singleline);
            Match match = regexp.Match(html);

            string newBody = WebUtility.HtmlDecode(match.Groups[1].Value);
            string[] newBodyLines = newBody.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);

            string oldBody = this.LatestHtml;
            string[] oldBodyLines = oldBody.Split(new string[] { Environment.NewLine }, StringSplitOptions.None);

            // 開始インデックスを取得
            int startIndex = 0;
            if (this.FilterStart != "") startIndex = newBody.IndexOf(this.FilterStart);
            if (startIndex < 0) startIndex = 0;
            // 終了インデックスを取得
            int endIndex = newBody.Length - 1;
            if (this.FilterEnd != "") endIndex = newBody.IndexOf(this.FilterEnd, startIndex);
            if (endIndex < 0) endIndex = newBody.Length - 1;

            // 差分確認
            DiffOption option = new DiffOption();
            option.TrimSpace = true;
            option.IgnoreSpace = true;
            option.IgnoreCase = false;
            DiffResult[] diffs = FastDiff.Diff(this.LatestHtml, newBody, option);

            // フィルタ条件を取得
            string[] includes = this.FilterInclude.Split(new char[]{','}, StringSplitOptions.RemoveEmptyEntries);
            string[] ignores = this.FilterIgnore.Split(new char[]{','}, StringSplitOptions.RemoveEmptyEntries);


            string diffStr = "";
            for (int i = 0; i < diffs.Length; i++)
            {
                // 変更なしの場合は無視
                if (!diffs[i].Modified)
                    continue;

                // フィルタ範囲内に無い場合は無視
                if (diffs[i].ModifiedStart < startIndex ||
                    diffs[i].ModifiedStart + diffs[i].ModifiedLength > endIndex)
                    continue;

                // 変更前後の文字列取得
                //string newLine = body.Substring(diffs[i].ModifiedStart, diffs[i].ModifiedLength).Trim();
                //string oldLine = body.Substring(diffs[i].OriginalStart, diffs[i].OriginalLength).Trim();
                //if (newLine == "" || oldLine == "")
                //    continue; // 空白のみの場合は無視
                string newLine = newBodyLines[diffs[i].ModifiedStart];
                string oldLine = oldBodyLines[diffs[i].OriginalStart];
                



                // 差分行に対象文字列が含まれているか判定
                // 未設定の場合は何もしない
                bool found = false;
                if (includes.Length > 0)
                {
                    foreach (string include in includes)
                    {
                        Regex reg = new Regex(include,
                            RegexOptions.IgnoreCase | RegexOptions.Singleline);
                        Match m = regexp.Match(newLine);

                        if (m.Success)
                        {
                            found = true;
                            break;
                        }
                    }
                    if (!found) continue;
                }

                // 差分行に無視文字列が含まれているか判定
                // 未設定の場合は何もしない
                if (ignores.Length > 0)
                {
                    foreach (string ignore in ignores)
                    {
                        Regex reg = new Regex(ignore,
                            RegexOptions.IgnoreCase | RegexOptions.Singleline);
                        Match m = regexp.Match(newLine);

                        if (m.Success)
                        {
                            found = true;
                            break;
                        }
                    }
                    if (found) continue;
                }

                // 変更差分を保存
                diffStr +=
                    oldLine + Environment.NewLine +
                    "----" + Environment.NewLine +
                    newLine + Environment.NewLine +
                    Environment.NewLine;

            }

            // 変更検出した場合は更新情報を設定してtrueを返す
            this.CheckedDate = DateTime.Now;
            if (diffStr != "")
            {
                this.LatestDate = this.CheckedDate;
                this.LatestHtml = newBody;
                this.LatestDiff = diffStr;

                return true;
            }
            else
            {
                return false;
            }
       }




        #region ページ文字列処理

        /// <summary>
        /// 指定したURLのページのHTMLを取得する。
        /// HTMLエンコード文字はデコードされない。
        /// </summary>
        /// <param name="url">取得対象ページのURL。</param>
        /// <returns>ページのHTML。</returns>
        public static string GetPageHtml(string url)
        {
            HttpWebRequest req = (HttpWebRequest)HttpWebRequest.Create(url);
            req.AllowAutoRedirect = true;
            req.Timeout = 10000;

            byte[] pageBytes;

            using (HttpWebResponse res = (HttpWebResponse)req.GetResponse())
            using (Stream stream = res.GetResponseStream())
            using (MemoryStream ms = new MemoryStream())
            {
                int readedLength = 0;
                byte[] readBytes = new byte[256];
                do
                {
                    // ストリームからバッファの読み込み
                    readedLength = stream.Read(readBytes, 0, readBytes.Length);
                    // バッファをメモリストリームへ書き込み
                    ms.Write(readBytes, 0, readedLength);

                } while (readedLength > 0);

                pageBytes = ms.ToArray();
            }

            // 文字コード取得
            Encoding enc = GetCode(pageBytes);
            // 文字列取得
            //string encStr = enc.GetString(pageBytes);
            // 改行コードを統一して文字列取得
            StringBuilder sb = new StringBuilder("");
            using (StreamReader sr = new StreamReader(new MemoryStream(pageBytes), enc))
            {
                string line;
                while ((line = sr.ReadLine()) != null)
                    sb.AppendLine(line);
            }

            return sb.ToString();

        }

        /// <summary>
        /// 文字コードを判別する
        /// </summary>
        /// <remarks>
        /// Jcode.pmのgetcodeメソッドを移植したものです。
        /// Jcode.pm(http://openlab.ring.gr.jp/Jcode/index-j.html)
        /// Jcode.pmのCopyright: Copyright 1999-2005 Dan Kogai
        /// </remarks>
        /// <param name="bytes">文字コードを調べるデータ</param>
        /// <returns>適当と思われるEncodingオブジェクト。
        /// 判断できなかった時はnull。</returns>
        private static Encoding GetCode(byte[] bytes)
        {
            const byte bEscape = 0x1B;
            const byte bAt = 0x40;
            const byte bDollar = 0x24;
            const byte bAnd = 0x26;
            const byte bOpen = 0x28;    //'('
            const byte bB = 0x42;
            const byte bD = 0x44;
            const byte bJ = 0x4A;
            const byte bI = 0x49;

            int len = bytes.Length;
            byte b1, b2, b3, b4;

            //Encode::is_utf8 は無視

            bool isBinary = false;
            for (int i = 0; i < len; i++)
            {
                b1 = bytes[i];
                if (b1 <= 0x06 || b1 == 0x7F || b1 == 0xFF)
                {
                    //'binary'
                    isBinary = true;
                    if (b1 == 0x00 && i < len - 1 && bytes[i + 1] <= 0x7F)
                    {
                        //smells like raw unicode
                        return System.Text.Encoding.Unicode;
                    }
                }
            }
            if (isBinary)
            {
                return null;
            }

            //not Japanese
            bool notJapanese = true;
            for (int i = 0; i < len; i++)
            {
                b1 = bytes[i];
                if (b1 == bEscape || 0x80 <= b1)
                {
                    notJapanese = false;
                    break;
                }
            }
            if (notJapanese)
            {
                return System.Text.Encoding.ASCII;
            }

            for (int i = 0; i < len - 2; i++)
            {
                b1 = bytes[i];
                b2 = bytes[i + 1];
                b3 = bytes[i + 2];

                if (b1 == bEscape)
                {
                    if (b2 == bDollar && b3 == bAt)
                    {
                        //JIS_0208 1978
                        //JIS
                        return System.Text.Encoding.GetEncoding(50220);
                    }
                    else if (b2 == bDollar && b3 == bB)
                    {
                        //JIS_0208 1983
                        //JIS
                        return System.Text.Encoding.GetEncoding(50220);
                    }
                    else if (b2 == bOpen && (b3 == bB || b3 == bJ))
                    {
                        //JIS_ASC
                        //JIS
                        return System.Text.Encoding.GetEncoding(50220);
                    }
                    else if (b2 == bOpen && b3 == bI)
                    {
                        //JIS_KANA
                        //JIS
                        return System.Text.Encoding.GetEncoding(50220);
                    }
                    if (i < len - 3)
                    {
                        b4 = bytes[i + 3];
                        if (b2 == bDollar && b3 == bOpen && b4 == bD)
                        {
                            //JIS_0212
                            //JIS
                            return System.Text.Encoding.GetEncoding(50220);
                        }
                        if (i < len - 5 &&
                            b2 == bAnd && b3 == bAt && b4 == bEscape &&
                            bytes[i + 4] == bDollar && bytes[i + 5] == bB)
                        {
                            //JIS_0208 1990
                            //JIS
                            return System.Text.Encoding.GetEncoding(50220);
                        }
                    }
                }
            }

            //should be euc|sjis|utf8
            //use of (?:) by Hiroki Ohzaki <ohzaki@iod.ricoh.co.jp>
            int sjis = 0;
            int euc = 0;
            int utf8 = 0;
            for (int i = 0; i < len - 1; i++)
            {
                b1 = bytes[i];
                b2 = bytes[i + 1];
                if (((0x81 <= b1 && b1 <= 0x9F) || (0xE0 <= b1 && b1 <= 0xFC)) &&
                    ((0x40 <= b2 && b2 <= 0x7E) || (0x80 <= b2 && b2 <= 0xFC)))
                {
                    //SJIS_C
                    sjis += 2;
                    i++;
                }
            }
            for (int i = 0; i < len - 1; i++)
            {
                b1 = bytes[i];
                b2 = bytes[i + 1];
                if (((0xA1 <= b1 && b1 <= 0xFE) && (0xA1 <= b2 && b2 <= 0xFE)) ||
                    (b1 == 0x8E && (0xA1 <= b2 && b2 <= 0xDF)))
                {
                    //EUC_C
                    //EUC_KANA
                    euc += 2;
                    i++;
                }
                else if (i < len - 2)
                {
                    b3 = bytes[i + 2];
                    if (b1 == 0x8F && (0xA1 <= b2 && b2 <= 0xFE) &&
                        (0xA1 <= b3 && b3 <= 0xFE))
                    {
                        //EUC_0212
                        euc += 3;
                        i += 2;
                    }
                }
            }
            for (int i = 0; i < len - 1; i++)
            {
                b1 = bytes[i];
                b2 = bytes[i + 1];
                if ((0xC0 <= b1 && b1 <= 0xDF) && (0x80 <= b2 && b2 <= 0xBF))
                {
                    //UTF8
                    utf8 += 2;
                    i++;
                }
                else if (i < len - 2)
                {
                    b3 = bytes[i + 2];
                    if ((0xE0 <= b1 && b1 <= 0xEF) && (0x80 <= b2 && b2 <= 0xBF) &&
                        (0x80 <= b3 && b3 <= 0xBF))
                    {
                        //UTF8
                        utf8 += 3;
                        i += 2;
                    }
                }
            }
            //M. Takahashi's suggestion
            //utf8 += utf8 / 2;

            System.Diagnostics.Debug.WriteLine(
                string.Format("sjis = {0}, euc = {1}, utf8 = {2}", sjis, euc, utf8));
            if (euc > sjis && euc > utf8)
            {
                //EUC
                return System.Text.Encoding.GetEncoding(51932);
            }
            else if (sjis > euc && sjis > utf8)
            {
                //SJIS
                return System.Text.Encoding.GetEncoding(932);
            }
            else if (utf8 > euc && utf8 > sjis)
            {
                //UTF8
                return System.Text.Encoding.UTF8;
            }

            return null;
        }

        public bool Diff(string newHtml)
        {
            DiffOption option = new DiffOption();
            option.TrimSpace = true;
            option.IgnoreSpace = true;
            option.IgnoreCase = false;

            DiffResult[] results = FastDiff.Diff(this.LatestHtml, newHtml, option);
            if (results.Length == 0) return false;

            string diffStr = "";

            for (int i = 0; i < results.Length; i++)
            {
                diffStr += this.LatestHtml.Substring(results[i].OriginalStart, results[i].OriginalLength);
                diffStr += Environment.NewLine + "----" + Environment.NewLine;
                diffStr += newHtml.Substring(results[i].ModifiedStart, results[i].ModifiedLength);
                diffStr += Environment.NewLine + "----" + Environment.NewLine;
            }

            this.LatestHtml = newHtml;
            this.LatestDiff = diffStr;
            return true;
 
        }


        #endregion



    }
}
